//
// CrackSelfUpdater
//
// Copyright (C) jextra.net.
//
//  This file is part of the Crack build system.
//
//  The Crack build system is free software; you can redistribute it 
//  and/or modify it under the terms of the GNU Lesser General Public
//  License as published by the Free Software Foundation; either
//  version 2.1 of the License, or (at your option) any later version.
//
//  The Crack build system is distributed in the hope that it will be 
//  useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
//  Lesser General Public License for more details.
//
//  You should have received a copy of the GNU Lesser General Public
//  License along with the Crack build system; if not, write to the Free
//  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
//  02111-1307 USA.
//

package net.jextra.crack.core;

import java.io.*;
import java.net.*;
import java.security.*;
import java.util.*;
import net.jextra.crack.*;
import net.jextra.crack.tool.*;
import net.jextra.crack.updater.*;

/**
 * Client code that communicates to a Crack Updater to get latest versions of libraries and plugins.
 * 
 * @see CrackAdmin
 */
public class CrackSelfUpdater
{
    // ============================================================
    // Fields
    // ============================================================

    public static final String PROPS_CRACKUPDATER = ".crackupdater.properties";
    public static final int BUFFER_SIZE = 50000;

    private CrackUpdater updater;
    private File crackHomeDir;
    private HashMap<String, File> localFiles;
    private HashMap<String, CrackUpdaterEntry> remoteFiles;
    private int numDeleted;
    private int numAcquired;

    // ============================================================
    // Constructors
    // ============================================================

    public CrackSelfUpdater( CrackUpdater updater )
    {
        this.updater = updater;
    }

    // ============================================================
    // Methods
    // ============================================================

    // ----------
    // public
    // ----------

    public void execute( File localDir, String remoteDir )
        throws Exception
    {
        crackHomeDir = CrackContext.getInstance().getCrackHome();
        localFiles = new HashMap<String, File>();
        remoteFiles = new HashMap<String, CrackUpdaterEntry>();
        numDeleted = 0;
        numAcquired = 0;

        System.out.printf( "Checking with Crack Updater for updates.\n" );

        scanLocalDir( localDir );
        scanRemoteDir( remoteDir );

        deleteMissingFiles();
        downloadOutdatedFiles();

        System.out.println();
        if ( numDeleted == 0 && numAcquired == 0 )
        {
            System.out.println( "Nothing has changed." );
        }

        if ( numDeleted > 0 )
        {
            System.out.printf( "%d files were deleted.", numDeleted );
        }

        if ( numAcquired > 0 )
        {
            System.out.printf( "%d files were acquired.", numAcquired );
        }
    }

    // ----------
    // private
    // ----------

    private void scanLocalDir( File dir )
        throws IOException, NoSuchAlgorithmException
    {
        for ( File file : dir.listFiles() )
        {
            if ( file.isFile() )
            {
                String path = FileTool.getRelativePath( crackHomeDir, file );
                localFiles.put( path, file );
            }
            else
            {
                scanLocalDir( file );
            }
        }
    }

    private void scanRemoteDir( String dirPath )
        throws Exception
    {
        for ( CrackUpdaterEntry entry : updater.getEntries( dirPath ) )
        {
            if ( entry.isFile() )
            {
                remoteFiles.put( entry.getPath(), entry );
            }
            else
            {
                scanRemoteDir( entry.getPath() );
            }
        }
    }

    private void deleteMissingFiles()
    {
        for ( String path : localFiles.keySet() )
        {
            if ( !remoteFiles.containsKey( path ) )
            {
                System.out.printf( "Deleting file [%s].\n", path );
                numDeleted++;

                File file = new File( crackHomeDir, path );
                file.delete();
            }
        }
    }

    private void downloadOutdatedFiles()
        throws Exception
    {
        for ( String path : remoteFiles.keySet() )
        {
            if ( filesDiffer( path ) )
            {
                System.out.printf( "Acquiring file [%s].\n", path );
                numAcquired++;

                downloadFile( path );
            }
        }
    }

    private boolean filesDiffer( String path )
        throws NoSuchAlgorithmException, IOException
    {
        File localFile = localFiles.get( path );
        CrackUpdaterEntry entry = remoteFiles.get( path );

        // If local file does not exist, they are obviously different.
        if ( localFile == null )
        {
            return true;
        }

        // If the lengths are different, all bets are off, they are different.
        if ( localFile.length() != entry.getSize() )
        {
            return true;
        }

        // If the hash is different than they are different.
        if ( !CrackUpdaterEntry.getSHA256Hash( localFile ).equalsIgnoreCase( entry.getSHA256Hash() ) )
        {
            return true;
        }

        return false;
    }

    private void downloadFile( String path )
        throws Exception
    {
        URL url = updater.getFileURL( path );
        URLConnection conn = url.openConnection();
        conn.setConnectTimeout( updater.getTimeout() );
        conn.setReadTimeout( updater.getTimeout() );
        conn.connect();
        InputStream in = conn.getInputStream();

        File file = new File( crackHomeDir, path );
        file.getParentFile().mkdirs();
        FileOutputStream out = new FileOutputStream( file );

        byte[] buf = new byte[BUFFER_SIZE];
        int i = 0;
        while ( ( i = in.read( buf ) ) != -1 )
        {
            out.write( buf, 0, i );
        }
        in.close();
        out.close();
    }
}
